function fmtHEX(v, l = 2) {
  return v !== undefined ? v.toString(16).padStart(l, 0).toLocaleUpperCase() : '<i class="c31">未定义</i>'
}

function FMT_Fixed(v, precise = 3) {
  return v === undefined ? '未定义' : (v === null ? '-' : v.toFixed(precise))
}

function XFMT_Fixed(precise = 3) {
  function dec(v) {
    return v === undefined ? '未定义' : (v === null ? '-' : (v == 0 ? '0' : v.toFixed(precise)))
  }

  return dec
}

function BJT(timestamp_secondes) {
  return new Date(timestamp_secondes * 1000 + 8 * 36e5).toISOString().slice(0, 23).replace('T', ' ')
}

let getStation = () => {
  return window.parent.document.getElementById('src').value
}

let $REAL_FLAG = true
let $STATION = getStation()

window.addEventListener('message', function (e) {
  console.log('[receive message]', e.data)
  if (main) main.change_station(e.data); else ($REAL_FLAG ? realtime : history)()
});

class Task {
  REG_TASK = /\/(?<s>.+)\/(?<station>\w.+?)\/(?<day>(?<y>20\d{2})(?<m>\d{2})(?<d>\d{2}))/

  constructor(ss) {
    Object.assign(this, this.REG_TASK.exec(ss).groups)
    let s = ss.split('/')
    this.task = ss
    this.label = `${s[s.length - 2]} / ${s[s.length - 3]}`
  }

  date() {
    return `${this.y}-${this.m}-${this.d}`;
  }

  option() {
    return `<option value="${this.task}">${this.label}</option>`
  }
}

st_frames = null
let MODE = null

class Realtime {
  constructor(key, station, params = []) {
    this._params = params.map($param)
    this.btnDisableCss = 'disactive-btn'
    this.key = key
    this.station = station
    this.frames_ori = []
    this.frames = [];
    this.tasks = []
    this.viewer = document.body
    this.$info = $$('#params');
    this.$task = $$('#task');
    this.$day = $$('#day') || {};
    this.$frmi = $$('#frm_input') || {}
    this.$frmr = $$('#frm_range') || {}
    this.$frmx = $$('#frm_max') || {};
    this.$mode = $$('#modes') || {}
    this.$history = $$('#history')
    this.$realtime = $$('#realtime')
    this.$day.onchange = this.load_task.bind(this)
    this.$task.onchange = this.onTask.bind(this)
    this.$frmi.onchange = this.onFrmi.bind(this)
  }

  async onDay() {
    this.close();
    this.reset();
    await this.load_task();
  }

  async onTask() {
    this.close()
    await this.load_frames(this.$task.value)
    if (this.frames.length < 1) {
      this.reset()
    }
  }

  onFrmi(i) {
    i = parseInt(this.$frmi.value)
    let min = parseInt(this.$frmr.min), max = parseInt(this.$frmr.max);
    if (i < min) i = min;
    if (i > max) i = max
    this.$frmi.textContent = this.$frmi.value = this.$frmr.value = i
    this.$frmr.onchange()
  }

  reset() {
    this.frames_ori = []
    this.frames = []
    if (this.$info) this.$info.innerHTML = ''
    this.$frmx.innerText = this.$frmi.textContent = ''
    this.frame_index = 0
    this.frame_max = 0
    this.seek(0)
  }

  params(v) {
    let i = v.info;
    this.$info.innerHTML = this._params.map(c => c.view_tr(i)).join('')
  }

  set frame_max(max) {
    if (max < 0) {
      max = 0
      this.$frmx.innerText = '';
    } else {
      this.$frmx.innerText = max;
    }
    this.$frmr.max = max
    return max
  }

  set frame_index(i) {
    i = parseInt(i)
    this.$frmi.textContent = this.$frmi.value = this.$frmr.value = i
    return i;
  }

  seek(i) {
    this.cur_frame = this.frames[i]
    if (!this.cur_frame) return
    this.cur_index = i
    this.$frmi.value = this.$frmr.value = i
    this.params(this.cur_frame);
  }

  get mode() {
    return this.$mode ? this.$mode.value : null
  }

  realtime() {
    this.connect()
  }

  async history() {
    this.close()
    await this.load_task()
  }

  change_station(station) {
    this.station = station;
    this.cur_task = ''
    this.task = {}
    this.reset()
    if (this.ws) this.realtime(); else this.history();

  }

  async load_task() {
    this.reset()
    $([this.$task, this.$mode]).html('')
    this.$task.innerHTML = ''
    let day = this.$day.value;
    if (!day) return
    this.tasks = await $api(`omsp.tasks?ins=${this.key}/${this.station}&day=${day}`)
    let $t = this.$task;
    $($t).html(this.tasks.map(v => new Task(v).option()))
    this.$task.value = this.cur_task;
    this.cur_task = this.$task.value;
    if (!this.cur_task) {
      this.$task.selectedIndex = 0
      this.cur_task = this.$task.value;
    }
    if (this.cur_task) {
      await this.load_frames(this.cur_task)
      this.frame_index = this.$frmr.max
      this.seek(this.$frmr.value)
    }
  }

  renderMode() {
    if (st_frames) {
      st_frames(this.frames_ori)
      return
    }
    if (this.$mode.constructor == HTMLSelectElement && MODE) {
      let r = {};
      let selected = this.$mode.value
      this.frames_ori.forEach((v) => r[v.mode] = (r[v.mode] || 0) + 1)
      let fr = Object.keys(r).filter(v => (MODE[v] && [v])).map(v => `<option value="${v}">${MODE[v]} / ${r[v]}</option>`)
      $('#modes').html((this.ws ? `<option value="">不限</option>` : '') + fr.join(''));

      if (selected && r[selected]) {
        this.$mode.value = selected
        return
      }
      let modeK = Object.keys(r)
      if (modeK.length > 0 && (!this.ws)) {
        this.$mode.value = modeK[0]
      }
    }
  }

  onMode() {
    this.close()
    this.renderMode()
    this.filter_frames(this.mode)
    this.seek(this.$frmr.value)
  }

  filter_frames(mode) {
    this.frames = mode ? this.frames_ori.filter(
      v => (mode === undefined || v.mode == mode)
    ).map(
      (v, i) => Object.assign(v, {frm_: i})
    ) : this.frames_ori.map((v, i) => Object.assign(v, {frm_: i}))
    this.resetFrmInput()
  }

  resetFrmInput() {
    if (this.frames.length > 0) {
      this.frame_index = this.frame_max = this.frames.length - 1
      return
    }
    this.$frmx.innerText = ''
    this.frame_max = 0
    this.frame_index = 0
  }

  async load_frames(task) {
    this.frames = this.frames_ori = []
    try {
      let resp = await $api(`omsp.frames?f=${task}`, null, v => v.text())
      let tmp = resp.split('\n').filter(v => !!v).reduce((acc, v) => {
        try {
          let iv = JSON.parse(v)
          acc.push(iv)
        } catch {
          console.warn(v)
        } finally {
          return acc
        }
      }, [])
      this.task_ = tmp[0]
      this.frames_ori = tmp.slice(1)
    } finally {
      this.renderMode()
      this.filter_frames(this.mode)
    }
  }

  set cur_task(t) {
    if (t) {
      this._cur_task = t
      let tsk = new Task(t);
      this.task = tsk
      this.$day.value = tsk.date();
    } else {
      this._cur_task = ''
      this.task = {}
    }
  }

  get cur_task() {
    return this._cur_task
  }

  async newtask(d) {
    this.cur_task = d.task
    this.frames_ori = []
    await this.load_task()
    return this.task
  }

  newframe(d) {
    if (!this.frames_ori.includes(d)) this.frames_ori.push(d)
    this.cur_frame = d
    return d
  }

  async msg(v) {
    let d = JSON.parse(v.data)
    if (d.task) return await this.newtask(d)
    return await this.newframe(d)
  }


  connect() {
    if (this.ws) this.close()
    this.ws = new WsEx(`omsp.realtime?ins=${this.key}/${this.station}`)
    this.ws.onmessage = this.msg.bind(this)
    this.ws.onerror = this.error.bind(this)
    // if (this.$realtime) this.$realtime.classList.add(this.btnDisableCss)
    // if (this.$history) this.$history.classList.remove(this.btnDisableCss)
    if (this.$realtime) this.$realtime.disabled = true;
    if (this.$history) this.$history.disabled = false;
  }


  error() {
    this.close()
    if (this.timeoutId) clearTimeout(this.timeoutId)
    this.timeoutId = setTimeout(this.connect.bind(this), 3000)
  }

  close() {
    // if (this.$history) this.$history.classList.add(this.btnDisableCss)
    // if (this.$realtime) this.$realtime.classList.remove(this.btnDisableCss)
    if (this.$history) this.$history.disabled = true
    if (this.$realtime) this.$realtime.disabled = false

    if (this.ws) this.ws.close()
    delete this.ws
  }

  exportCSV(btn, key) {
    btn.disabled = true
    let a = null;
    try {
      let params = key ? this._params[key] : this._params;
      let title = params.map(v => v.csv_h).join(',')
      let data = [new Uint8Array([0xEF, 0xBB, 0xBF]), `时间,${title}\n`];
      this.frames.forEach(v => {
        let info = v.info
        let p = key ? (info[key] ? info[key] : info) : info;
        let line = params.map(c => c.csv_b(p)).join(",")
        // data.push(`${BJT(v.st)},${line}\n`)
        data.push(`"${BJT(v.st)}",${line}\n`)
      })
      let blob = new Blob(data, {'content_type': 'application/csv;charset=utf8'});
      let a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      let t = this.task;
      // $name = `工参_${sta}_${pkg}_${track}_${arc}_${$TS.value.replace(/[-:T \/]/g, '').slice(0, 14)}`
      let n = main.cur_task.split('/')
      let [station, orbit, instrument] = n.slice(n.length - 3)
      // 获取当前选中的模式文本（图像模式/工作模式），去掉“/ 数量”，过滤“不限”，并清理非法字符
      let modeLabel = ''
      if (this.$mode && this.$mode.options && this.$mode.selectedIndex >= 0) {
        modeLabel = this.$mode.options[this.$mode.selectedIndex].text || ''
      }
      modeLabel = modeLabel.replace(/\s*\/.*$/, '').trim()
      if (!modeLabel || /^不限$/.test(modeLabel)) modeLabel = ''
      const sanitize = s => s.replace(/[\\\/:*?"<>|]/g, '_')
      let modeStr = modeLabel ? `_${sanitize(modeLabel)}` : ''
      a.download = `科学数据_${station}_${instrument}_${modeStr}_${orbit}.csv`
      a.click()
    } finally {
      if (a) {
        setTimeout(() => {
          URL.revokeObjectURL(a.href)
        }, 1000)
      }
      btn.disabled = false
    }
  }
}

class Param {
  constructor(x) {
    this.k = x.k
    this.n = x.n
    this.u = x.u || x.unit
    this.v = x.v || (v => v)
    this.r = x.r ? (x.r.constructor === String ? new RawFmt(x.r) : x.r) : null
    this.c = x.c || (v => 1)
  }

  fmt(v, d) {
    if (v === null) return ' - '
    if (v === undefined) return 'undef'
    return this.v(v, d)
  }

  get csv_h() {
    return this.u ? `"${this.n.replace(/(^[+,\-,=,*])/, ' $1')}/(${this.u})","${this.k.replace(/(^[+,\-,=,*])/, ' $1')}"` : `"${this.n.replace(/(^[+,\-,=,*])/, ' $1')}","${this.k.replace(/(^[+,\-,=,*])/, ' $1')}"`
  }

  csv_b(d) {
    let v = d[this.k], r = this.raw(d);
    return `"${this.fmt(v, d)}","${r}"`
  }


  raw(d) {
    let r = d[this.k + '_']
    r = r === undefined ? d[this.k] : r;
    if (r === undefined) return `未定义 ${this.k}`;
    return (r.constructor === String) ? r : (this.r ? this.r.fmt(r) : '?' + r);
  }

  view_tr(d) {
    let v = d[this.k], r = this.raw(d);
    let c = (this.c ? this.c(v) : (v !== undefined)) ? '' : ' class="c31"'
    /*return `<tr ${c}><th>${this.n}</th><td>${this.fmt(v, d)} <kbd>${r}</kbd></td></tr>`*/
    //】
    return `<tr ${c}><th>${this.n}</th><td>${this.fmt(v, d)} <kbd>${r}</kbd></td></tr>`
  }

  table_td(d) {
    let v = d[this.k], r = this.raw(d);
    let c = (this.c ? this.c(v) : (v !== undefined)) ? '' : ' class="c31"'
    return `<td>${this.fmt(v, d)} 【${r}】</td>`
  }
}

let $param = (x) => new Param(x)

$create = (v, options = {}, inner, namespaceURL) => {
  let el
  if (namespaceURL)
    el = document.createElementNS(namespaceURL, v)
  else
    el = document.createElement(v)
  options.items(([k, v]) => el[k] = v);
  if (inner) el.inner = inner;
  return el;
}

function $dld(task, name) {
  window.location.href = `/-/omsp.dld?t=${task}&n=${name}`
}

function padBlank(v) {
  return v === undefined ? '-' : v
}

function showV(params, d, k) {
  return params[k] ? (params[k][d[k]] || '') : '-'
}

class RawFmt {
  constructor(fmt) {
    this.size = parseInt(fmt)
    let t = fmt[fmt.length - 1];
    this.fmt = this[t] || (() => `?RawFmt 定义错误 ${fmt}`)
  }

  X(v) {
    if (v < 0) {
      // console.warn("convert before" +v)
      v = v + (1 << (this.size * 4))
      // console.warn("convert after" +v)
    }
    return '0x' + v.toString(16).toUpperCase().padStart(this.size, '0')
  }

  b(v) {
    return '0b' + v.toString(2).padStart(this.size, '0')
  }
}



